import { Collection, Map, MapBrowserEvent, PluggableMap, View } from 'ol';
import { unByKey } from 'ol/Observable';
import {
  Control, FullScreen, MousePosition, OverviewMap, ScaleLine, ZoomSlider, ZoomToExtent, defaults as defaultControls
} from 'ol/control';
import { Options as ControlOptions } from 'ol/control/Control';
import { toStringXY } from 'ol/coordinate';
import { EventsKey } from 'ol/events';
import { applyTransform } from 'ol/extent';
import { GeoJSON, MVT } from 'ol/format';
import GeometryType from 'ol/geom/GeometryType';
import { Draw, Modify, Select, defaults as defaultInteractions } from 'ol/interaction';
import { Tile as TileLayer, Vector as VectorLayer, VectorTile as VectorTileLayer } from 'ol/layer';
import { get as getProjection, getTransform } from 'ol/proj';
import { register } from 'ol/proj/proj4';
import { OSM, Vector as VectorSource, VectorTile as VectorTileSource } from 'ol/source';
import { Circle, Fill, Stroke, Style } from 'ol/style';
import { StyleFunction } from 'ol/style/Style';

import proj4 = require('proj4');
/**
 * ==================================================
 * # Styles
 * ==================================================
 */

const image = new Circle({
  radius: 5,
  fill: null as any,
  stroke: new Stroke({ color: 'red', width: 1 })
});

const styles: { [key: string]: Style } = {
  Point: new Style({
    image
  }),
  LineString: new Style({
    stroke: new Stroke({
      color: 'green',
      width: 1
    })
  }),
  MultiLineString: new Style({
    stroke: new Stroke({
      color: 'green',
      width: 1
    })
  }),
  MultiPoint: new Style({
    image
  }),
  MultiPolygon: new Style({
    stroke: new Stroke({
      color: 'yellow',
      width: 1
    }),
    fill: new Fill({
      color: 'rgba(255, 255, 0, 0.1)'
    })
  }),
  Polygon: new Style({
    stroke: new Stroke({
      color: 'blue',
      lineDash: [4],
      width: 3
    }),
    fill: new Fill({
      color: 'rgba(0, 0, 255, 0.1)'
    })
  }),
  GeometryCollection: new Style({
    stroke: new Stroke({
      color: 'magenta',
      width: 2
    }),
    fill: new Fill({
      color: 'magenta'
    }),
    image: new Circle({
      radius: 10,
      fill: null as any,
      stroke: new Stroke({
        color: 'magenta'
      })
    })
  }),
  Circle: new Style({
    stroke: new Stroke({
      color: 'red',
      width: 2
    }),
    fill: new Fill({
      color: 'rgba(255,0,0,0.2)'
    })
  })
};

const styleFunction: StyleFunction = feature => styles[feature.getGeometry().getType()];

/**
 * ==================================================
 * # Vector
 * ==================================================
 */

const geojsonString = '{"type":"FeatureCollection","crs":{"type":"name","properties":{"name":"EPSG:3857"}},"features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4000000,-2000000],[8000000,2000000]]}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4000000,2000000],[8000000,-2000000]]}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-5000000,-1000000],[-4000000,1000000],[-3000000,-1000000]]]}},{"type":"Feature","geometry":{"type":"MultiLineString","coordinates":[[[-1000000,-750000],[-1000000,750000]],[[1000000,-750000],[1000000,750000]],[[-750000,-1000000],[750000,-1000000]],[[-750000,1000000],[750000,1000000]]]}},{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-5000000,6000000],[-5000000,8000000],[-3000000,8000000],[-3000000,6000000]]],[[[-2000000,6000000],[-2000000,8000000],[0,8000000],[0,6000000]]],[[[1000000,6000000],[1000000,8000000],[3000000,8000000],[3000000,6000000]]]]}},{"type":"Feature","geometry":{"type":"GeometryCollection","geometries":[{"type":"LineString","coordinates":[[-5000000,-5000000],[0,-5000000]]},{"type":"Point","coordinates":[4000000,-5000000]},{"type":"Polygon","coordinates":[[[1000000,-6000000],[2000000,-4000000],[3000000,-6000000]]]}]}}]}';

const vectorSource = new VectorSource({
  features: (new GeoJSON()).readFeatures(JSON.parse(geojsonString))
});

const vectorLayer = new VectorLayer({
  source: vectorSource,
  style: styleFunction,
  visible: true,
  zIndex: 10
});

const vectorTileSource = new VectorTileSource({
  format: new MVT(),
  url: 'https://basemaps.arcgis.com/v1/arcgis/rest/services/World_Basemap/VectorTileServer/tile/{z}/{y}/{x}.pbf'
});

const vectorTileLayer = new VectorTileLayer({
  source: vectorTileSource,
  visible: false,
  zIndex: 9
});

/**
 * ==================================================
 * # Tile
 * ==================================================
 */

const osmLayer = new TileLayer({
  source: new OSM({
    crossOrigin: 'anonymous'
  }),
  visible: true,
  zIndex: 0
});

/**
 * ==================================================
 * # Controls
 * ==================================================
 */

const layers = [
  osmLayer,
  vectorLayer,
  vectorTileLayer
];

const controls = defaultControls().extend([
  new FullScreen(),
  new MousePosition({
    coordinateFormat: coord => toStringXY(coord, 8),
    undefinedHTML: ''
  }),
  new OverviewMap({
    layers,
    view: new View({
      projection: 'EPSG:3857'
    })
  }),
  new ScaleLine(),
  new ZoomSlider(),
  new ZoomToExtent(),
]);

/**
 * ==================================================
 * # Interactions
 * ==================================================
 */

const drawInteractions: Draw[] = [];
(Object.keys(GeometryType) as (keyof typeof GeometryType)[]).forEach(type => {
  const draw = new Draw({
    type: GeometryType[type],
  });

  drawInteractions.push(draw);
});

const selectInteraction = new Select({
  features: new Collection(vectorSource.getFeatures())
});

const modifyInteraction = new Modify({
  features: selectInteraction.getFeatures()
});

const interactions = defaultInteractions()
  .extend([
    selectInteraction,
    modifyInteraction
  ]);

/**
 * ==================================================
 * # Map and View
 * ==================================================
 */

const view = new View({
  center: [0, 0],
  projection: 'EPSG:3857',
  zoom: 10
});

const map = new Map({
  target: document.getElementById('map') as HTMLElement,
  view,
  layers,
  controls,
  interactions
});

/**
 * ==================================================
 * # Reprojection
 * ==================================================
 */

const projSpec = {
  code: 'EPSG:4813',
  bbox: [
    -5.83,
    105.06,
    -8.91,
    115.77
  ],
  proj4: '+proj=longlat +ellps=bessel +towgs84=-377,681,-50,0,0,0,0 +pm=jakarta +no_defs',
  name: 'Batavia (Jakarta)'
};

proj4.defs(projSpec.code, projSpec.proj4);
register(proj4);

const proj = getProjection(projSpec.code);
const transform = getTransform('EPSG:4326', proj);
const extent = applyTransform([projSpec.bbox[1], projSpec.bbox[2], projSpec.bbox[3], projSpec.bbox[0]], transform);
proj.setExtent(extent);
const newView = new View({
  projection: proj
});

setTimeout(() => {
  map.setView(newView);
  newView.fit(extent);
}, 5000);

/**
 * ==================================================
 * # Custom Control
 * ==================================================
 */

interface CustomControlOptions extends ControlOptions {
  name?: string;
}

class CustomControl extends Control {
  element: HTMLElement;
  name: string;
  mapViewport?: HTMLElement;
  private readonly _boundListener: (e: Event) => void;
  private readonly _eventKeys: EventsKey[];

  constructor(options: CustomControlOptions = {}) {
    options.element = document.createElement('div');
    options.element.className = 'ol-custom-control ol-unselectable ol-control';
    super(options);
    this.name = options.name || this.constructor.name;
    this._boundListener = this._listener.bind(this);
    this._eventKeys = [];
  }

  // Override
  setMap(map: PluggableMap) {
    super.setMap(map);
    unByKey(this._eventKeys);
    this._eventKeys.splice(0);

    if (this.mapViewport)
      this.mapViewport.removeEventListener('click', this._boundListener);

    this.mapViewport = map ? map.getViewport() : undefined;

    if (!this.mapViewport) return;

    this.mapViewport.addEventListener('click', this._boundListener);
    const view = map.getView();
    this._eventKeys.push(
      view.on('change:center', evt => {
        console.log(evt.oldValue, view.getCenter());
      })
    );
  }

  private _listener(evt: MouseEvent) {
    const mapEvent = new MapBrowserEvent(evt.type, this.getMap(), evt);
    console.log(mapEvent);
  }
}

map.addControl(new CustomControl());
